package org.flexthinker.binding.value.support
{
    import mx.events.PropertyChangeEvent;

    import org.flexthinker.binding.value.CommitTrigger;
    import org.flexthinker.binding.value.CommitTriggerListener;
    import org.flexthinker.binding.value.PropertyChangeListener;
    import org.flexthinker.binding.value.ValueChangeDetector;
    import org.flexthinker.binding.value.ValueModel;
    import org.flexthinker.binding.value.ValueModelProxy;

    /**
     * A value model that wraps another value model; delaying or buffering changes
     * until a commit is triggered.
     */
    public class BufferedValueModel implements ValueModel, ValueModelProxy, CommitTriggerListener
    {
        private var wrappedModel:ValueModel;

        private var _bufferedValue:Object;

        protected var buffering:Boolean = false;

        private var _valueChangeDetector:ValueChangeDetector;

        private var wrappedChangeListeners:Vector.<PropertyChangeListener> = new Vector.<PropertyChangeListener>();

        private var wrappedModelChangeListener:PropertyChangeListener;

        private var commitTrigger:CommitTrigger;

        /**
         * Constructs a <code>BufferedValueHolder</code> that wraps the given wrappedModel.
         *
         * @param valueModel the value model to be buffered
         */
        public function BufferedValueModel( valueModel:ValueModel )
        {
            wrappedModel = valueModel;
            init();
        }

        private function init():void
        {
            _bufferedValue = wrappedModel.getValue();
            wrappedModelChangeListener = new DelegatingPropertyChangeListener( wrappedValueChangeHandler )
            wrappedModel.addValueChangeListener( wrappedModelChangeListener );
        }

        public function addValueChangeListener( listener:PropertyChangeListener ):void
        {
            wrappedChangeListeners.push( listener );
        }

        public function removeValueChangeListener( listener:PropertyChangeListener ):void
        {
            var idx:int = wrappedChangeListeners.indexOf( listener );
            if( idx >= 0 )
            {
                wrappedChangeListeners.splice( idx, 1 );
            }
        }

        public function removeAllListeners():void
        {
            wrappedModel.removeAllListeners();
            wrappedChangeListeners = new Vector.<PropertyChangeListener>();
        }

        /**
         * Returns whether this model buffers a value or not, that is, whether
         * a value has been assigned since the last commit or revert.
         *
         * @return true if a value has been assigned since the last commit or revert
         */
        public function isBuffering():Boolean
        {
            return buffering;
        }


        public function getValue():Object
        {
            return _bufferedValue;
        }

        /**
         * Sets a new buffered value and turns this BufferedValueModel into
         * the buffering state. The buffered value is not provided to the
         * underlying model until the trigger channel indicates a commit.
         *
         * @param value  the value to be buffered
         */
        public function setValue( value:Object ):void
        {
            if( !this.equals( value ) )
            {
                var oldValue:Object = getValue();
                _bufferedValue = value;
                updateBuffering();
                fireValueChange( oldValue, _bufferedValue );
            }
        }

        public function get bufferedValue():Object
        {
            return _bufferedValue;
        }


        public function equals( obj:Object ):Boolean
        {
            // TBD : there are cases when equals is treated in a custom manner
            // in the wrappedValueModel
            // 1. investigate wrappedModel.setValueSilently( obj ) the setValueSilentry( initialValue )
            // 2. investigate a way to expose valueChangeDetector and use it to compare
            /*
            var vh:ValueHolder = new ValueHolder( this, "bufferedValue" );
            return vh.equals( obj );
            */
            getValueChangeDetector().hasValueChanged( bufferedValue, obj );
        }

        /**
         * Returns the wrappedModel, i.e. the underlying ValueModel that provides
         * the unbuffered value.
         *
         * @return the ValueModel that provides the unbuffered value
         */
        public function getWrappedValueModel():ValueModel
        {
            return wrappedModel;
        }


        public function setSource(value:Object):void
        {
            wrappedModel.setSource(value);
        }

        public function getSource():Object
        {
            return wrappedModel.getSource();
        }

        public function commit():void
        {
            wrappedModel.removeValueChangeListener( wrappedModelChangeListener );
            wrappedModel.setValue( getValueToCommit() );
            wrappedModel.addValueChangeListener( wrappedModelChangeListener );
            setValue( wrappedModel.getValue() ); // check if the wrapped model changed the committed value
            updateBuffering();
        }

        public function revert(toSource:Object=null):void
        {
            if ( toSource != null )
            {
                wrappedModel.removeValueChangeListener( wrappedModelChangeListener );
                wrappedModel.setSource(toSource);
                wrappedModel.addValueChangeListener( wrappedModelChangeListener );
            }
            setValue( wrappedModel.getValue() );
        }

        /**
         * Sets the <code>CommitTrigger</code> that triggers the commit and revert method.
         * CommitTrigger is useful b/c FormModel calls it directly without
         *
         * @param commitTrigger  the commit trigger; or null to deregister the
         * existing trigger.
         */
        public function setCommitTrigger( commitTrigger:CommitTrigger ):void
        {
            if( this.commitTrigger == commitTrigger )
            {
                return;
            }

            if( this.commitTrigger != null )
            {
                this.commitTrigger.removeCommitTriggerListener( this );
                this.commitTrigger = null;
            }

            if( commitTrigger != null )
            {
                this.commitTrigger = commitTrigger;
                this.commitTrigger.addCommitTriggerListener( this );
            }
        }

        public function getValueChangeDetector():ValueChangeDetector
        {
            if (!_valueChangeDetector)
            {
                _valueChangeDetector = new EqualsValueChangeDetector();
            }
            return _valueChangeDetector;
        }

        public function setValueChangeDetector(value:ValueChangeDetector):void
        {
            _valueChangeDetector = value;
        }

        // -------------------
        //   internal methods
        // -------------------

        protected function getValueToCommit():Object
        {
            return _bufferedValue;
        }

        private function updateBuffering():void
        {
            var wasBuffering:Boolean = isBuffering();
            buffering = !wrappedModel.equals( _bufferedValue );
        }

        private function wrappedValueChangeHandler( event:PropertyChangeEvent ):void
        {
            setValue( wrappedModel.getValue() );
        }

        protected function fireValueChange( oldValue:Object, newValue:Object ):void
        {
            for each ( var listener:PropertyChangeListener in wrappedChangeListeners )
            {
                listener.propertyChange( new PropertyChangeEvent( PropertyChangeEvent.PROPERTY_CHANGE,
                                                                  false, false, null,
                                                                  "bufferedValue",
                                                                  oldValue, newValue, this ) );
            }
        }
    }
}